/*
 * Log.js: Unobtrusive logging facility
 *
 * This module defines a single global symbol: a function named log().
 * Log a message by calling this function with 2 or 3 arguments:
 * 
 *   category: the type of the message.  This is required so that messages
 *     of different types can be selectively enabled or disabled and so
 *     that they can be styled independently.  See below.
 *
 *   message: the text to be logged.  May be empty if an object is supplied
 *
 *   object: an object to be logged.  This argument is optional.  If passed,
 *     the properties of the object will be logged in the form of a table.
 *     Any property whose value is itself an object may be logged recursively.
 * 
 * Utility Functions:
 *
 *   The log.debug() and log.warn() functions are utilities that simply 
 *   call the log() function with hardcoded categories of "debug" and
 *   "warning".  It is trivial to define a utility that replaces the built-in
 *   alert() method with one that calls log().
 * 
 * Enabling Logging
 *
 *   Log messages are *not* displayed by default.  You can enable the
 *   display of messages in a given category in one of two ways.  The
 *   first is to create a <div> or other container element with an id
 *   of "<category>_log".  For messages whose category is "debug", you might
 *   place the following in the containing document:
 * 
 *      <div id="debug_log"></div>
 *
 *   In this case, all messages of the specified category are appended
 *   to this container, which can be styled however you like.
 * 
 *   The second way to enable messages for a given category is to
 *   set an appropriate logging option.  To enable the category
 *   "debug", you'd set log.options.debugEnabled = true.  When you
 *   do this, a <div class="log"> is created for the logging messages.
 *   If you want to disable the display of log messages, even if a container
 *   with a suitable id exists, set another option:
 *   log.options.debugDisabled=true.  Set this option back to false to 
 *   reenable log messages of that category.
 *
 * Styling Log Messages
 * 
 *   In addition to styling the log container, you can use CSS to
 *   style the display of individual log messages.  Each log message
 *   is placed in a <div> tag, and given a CSS class of
 *   <category>_message.  Debugging messages would have a class "debug_message"
 * 
 * Log Options
 * 
 *   Logging behavior can be altered by setting properties of the log.options
 *   object, such as the options described earlier to enable or disable logging
 *   for given categories.  A few other options are available:
 *
 *     log.options.timestamp: If this property is true, each log message
 *       will have the date and time added to it.
 *
 *     log.options.maxRecursion: An integer that specifies the maximum number
 *       of nested tables to display when logging objects.  Set this to 0 if
 *       you never want a table within a table.
 *
 *     log.options.filter: A function that filters properties out when logging
 *       an object.  A filter function is passed the name and value of
 *       a property and returns true if the property should appear in the
 *       object table or false otherwise.
 */
function log(category, message, object) {
    // If this category is explicitly disabled, do nothing
    if (log.options[category + "Disabled"]) return;

    // Find the container
    var id = category + "_log";
    var c = document.getElementById(id);

    // If there is no container, but logging in this category is enabled,
    // create the container.
    if (!c && log.options[category + "Enabled"]) {
        c = document.createElement("div");
        c.id = id;
        c.className = "log";
        document.body.appendChild(c);
    }

    // If still no container, we ignore the message
    if (!c) return;

    // If timestamping is enabled, add the timestamp
    if (log.options.timestamp)
        message = new Date() + ": " + (message?message:"");

    // Create a <div> element to hold the log entry
    var entry = document.createElement("div");
    entry.className = category + "_message";

    if (message) {
        // Add the message to it
        entry.appendChild(document.createTextNode(message));
    }

    if (object && typeof object == "object") {
        entry.appendChild(log.makeTable(object, 0));
    }

    // Finally, add the entry to the logging container
    c.appendChild(entry);
}

// Create a table to display the properties of the specified object
log.makeTable = function(object, level) {
    // If we've reached maximum recursion, return a Text node instead.
    if (level > log.options.maxRecursion)
        return document.createTextNode(object.toString());
    
    // Create the table we'll be returning
    var table = document.createElement("table");
    table.border = 1;
    
    // Add a Name|Type|Value header to the table
    var header = document.createElement("tr");
    var headerName = document.createElement("th");
    var headerType = document.createElement("th");
    var headerValue = document.createElement("th");
    headerName.appendChild(document.createTextNode("Name"));
    headerType.appendChild(document.createTextNode("Type"));
    headerValue.appendChild(document.createTextNode("Value"));
    header.appendChild(headerName);
    header.appendChild(headerType);
    header.appendChild(headerValue);
    table.appendChild(header);

    // Get property names of the object and sort them alphabetically
    var names = [];
    for(var name in object) names.push(name);
    names.sort();

    // Now loop through those properties
    for(var i = 0; i < names.length; i++) {
        var name, value, type;
        name = names[i];
        try {
            value = object[name];
            type = typeof value;
        }
        catch(e) { // This should not happen, but it can in Firefox
            value = "<unknown value>";
            type = "unknown";
        };

        // Skip this property if it is rejected by a filter
        if (log.options.filter && !log.options.filter(name, value)) continue;

        // Never display function source code: it takes up too much room
        if (type == "function") value = "{/*source code suppressed*/}";

        // Create a table row to display property name, type and value
        var row = document.createElement("tr");
        row.vAlign = "top";
        var rowName = document.createElement("td");
        var rowType = document.createElement("td");
        var rowValue = document.createElement("td");
        rowName.appendChild(document.createTextNode(name));
        rowType.appendChild(document.createTextNode(type));

        // For objects, recurse to display them as tables
        if (type == "object") 
            rowValue.appendChild(log.makeTable(value, level+1));
        else 
            rowValue.appendChild(document.createTextNode(value));

        // Add the cells to the row, and add the row to the table
        row.appendChild(rowName);
        row.appendChild(rowType);
        row.appendChild(rowValue);

        table.appendChild(row);
    }
    
    // Finally, return the table.
    return table;
}

// Create an empty options object
log.options = {};

// Utility versions of the function with hardcoded categories
log.debug = function(message, object) { log("debug", message, object); };
log.warn = function(message, object) { log("warning", message, object); };

// Uncomment the following line to convert alert() dialogs to log messages
// function alert(msg) { log("alert", msg); }
